스프링을 학습하고 제대로 활용하려면 최소한의 JUnit 프레임워크를 사용한 테스트 작성 방법과 실행 방법은 알고 있어야 한다!

테스트 결과의 일관성

테스트가 외부 상태에 따라 성공하기도 실패하기도 하면 안된다. 반복적으로 테스트를 했을 때 테스트가 실패하기도 하고 성공하기도 한다면 이는 좋은 테스트라고 할 수가 없다. 코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 한다.

deleteAll()getCount()추가

일관성 있는 결과를 보장하는 테스트를 만들기 위해 UserDao에 새로운 기능, deleteAll(), getCount()를 추가한다.

deleteAll()getCount()의 테스트

독립된 새로운 테스트를 만드는 것보다 addAndGet()테스트를 확장하는 방법을 사용하는 것이 나을 것 같다. addAndGet()테스트의 불편한 점은 실행 전에 수동으로 USER 테이블의 내용을 모두 삭제해줘야 하는 것이었다. 그러므로 addAndGet()메소드 시작 전에 deleteAll()메소드를 실행시킨다.

deleteAll() -> addAndGet()

하지만 deleteAll()자체의 검증이 안 됐는데 무턱대고 다른 테스트에 적용할 수 없다. 그러므로 getCount()를 함께 적용하자.

deleteAll() -> getCount()==0 -> addAndGet()

그런데 getCount()역시 잘 동작하는 지 믿을 수 없다. 그러므로 add()메소드를 실행한 뒤에 getCount()의 결과를 한 번 더 확인해보자.

deleteAll() -> getCount()==0 -> add() -> getCount()==1 -> addAndGet()

동일한 결과를 보장하는 테스트

이로써 테스트가 어떤 상황에서 반복적으로 실행된다고 하더라도 동일한 결과가 나올 수 있게 되었다. 다시 한번 말하지만, 단위 테스트는 코드가 바뀌지 않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 한다.

포괄적인 테스트

getCount()테스트

getCount()에 대한 좀 더 꼼꼼한 테스트를 만들어보자. 테스트 메소드는 한 번에 한 가지 검증 목적에만 충실한 것이 좋으니, 새로운 테스트 메소드를 만든다. 테스트 시나리오는 USER테이블의 데이터를 모두 지우고 getCount()로 레코드 개수가 0개 임을 확인한다. 그리고 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인하는 것이다.

@Test
public void count() throws SQLException {

    ...

     dao.deleteAll();
     assertThat(dao.getCount(), is(0));

     dao.add(user1);
     assertThat(dao.getCount(), is(1));

     dao.add(user2);
     assertThat(dao.getCount(), is(2));

     dao.add(user3);
     assertThat(dao.getCount(), is(3));

주의할 점은 JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다. 테스트 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다.

addAndGet()테스트 보완

get()메소드에 대한 테스트 기능을 좀 더 보완할 필요가 있다.

@Test
public void addAndGet() throws ClassNotFoundException, SQLException {

    ... 

    User userget1 = dao.get(user1.getId());
    assertThat(userget1.getName(), is(user1.getName()));
    assertThat(userget1.getPassword(), is(user1.getPassword()));  
    ...
}

첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.

get() 예외조건에 대한 테스트

해당하는 id에 대한 정보가 없을 때, 두 가지 방법이 있을 것이다. 하나는 null과 같은 특별한 값을 리턴하는 것이고, 다른 하나는 id에 해당하는 정보를 찾을 수 없다고 예외를 던지는 것이다. 여기서는 후자의 방법을 사용한다. 이번 테스트는 테스트 진행 중에 특정 예외가 던져지면 테스트가 성공한 것이고, 던져지지 않고 정상적으로 작업을 마치면 테스트가 실패했다고 판단해야 한다. 문제는 예외 발생 여뷰는 메소드를 실행해서 리턴 값을 비교하는 방법으로는 확인할 수 없다는 점이다. 즉 assertThat()메소드로는 검증이 불가능하다.

이런 경우를 위하여 @Test어노테이션의 expected엘리먼트를 달아 테스트 메소드 실행 중에 발생하리라 기대하는 예외 클래스를 넣어주면 된다.

테스트를 성공시키기 위한 코드의 수정

UserDao클래스에서 주어진 id에 해당하는 데이터가 없으면 EmptyResultDataAccessException을 던지는 get()메소드를 만들어 낸다.

포괄적인 테스트

이렇게 DAO의 메소드에 대한 포괄적인 테스트를 만들어두는 편이 훨씬 안전하고 유용하다. 개발자는 빨리 테스트를 만들어 성공하는 것을 보고 다음 기능으로 나아가고 싶어하기 때문에, 긍정적인 경우를 골라서 성공할 만한 테스트를 먼저 작성하게 되기 쉬운데 부정적인 케이스를 먼저 만드는 습관을 들여서 예외 상황을 빠드리지 않는 꼼꼼한 개발을 하는 것이 좋다.

테스트가 이끄는 개발

get()메소드의 예외 테스트를 만드는 과정을 돌이켜 보면, 테스트를 먼저 만들어 테스트가 실패하는 것을 보고 나서 UserDao의 코드에 손을 대기 시작했다. 이런 순서를 따라 개발을 진행하는 구체적인 개발 전략이 실제로 존재한다.

기능 설계를 위한 테스트

테스트할 코드도 없는데 어떻게 테스트를 만들 수 있었을까? 이는 어떻게 테스트할까라고 생각하면서 만든 게 아니라, 추가하고 싶은 기능을 코드로 표현하려고 했기 때문에 가능했다. 이는 보통 기능설계 구현 테스트라는 일반적인 개발 흐름의 기능설계에 해당하는 부분을 테스트 코드가 일부분 담당하고 있다고 볼 수 있다. 테스트가 성공한다면, 코드 구현과 테스트라는 두 가지 작업이 동시에 끝나는 것이다.

테스트 주도 개발

만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법을 TDD(테스트주도개발)이라고 한다. 개발자가 테스트를 만들어가며 개발하는 방법이 주는 장점을 극대화한 방법이고, "실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다"는 것이 TDD의 기본 원칙이다.

TDD는 아예 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어 낼 수 있다. 또한 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다. 그 덕에 피드백을 매우 빠르게 받을 수 있게 된다. 더불어 코드에 대한 확신을 가질 수 있어, 가벼운 마음으로 다음단계로 넘어갈 수가 있다.

TDD에서는 테스트를 작성하고 이를 성공시키는 코드를 만드는 주기를 가능한 짧게 가져가도록 권장한다. 또한 TDD를 하면 자연스럽게 단위 테스트를 만들 수 있다.

사실 대부분의 개발자들은 무의식적으로 이미 테스트가 개발을 이끌어가는 방식으로 개발을 하고 있다고 생각한다. 문제는 이게 머릿속에서만 이루어지면 오류가 많고 나중에 다시 반복하기가 힘들다는 것인데, 차라리 머릿속에서 이루어지는 복잡한 작업을 끄집어 내놓으면 이게 바로 TDD가 된다.

장점 중 하나는 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧아 오류를 빨리 수정할 수 있다.

지연이 염려될까 걱정하지 않아도 된다. 오히려 빨라진다. 그런데 왜 개발자가 테스트를 잘 만들지 않는 것일까? 이유는 기업용 앱의 테스트를 만들기가 매우 어렵다고 생각하기 때문이다. 하지만 스프링은 엔터프라이즈 어플리케이션 테스트를 빠르고 쉽게 작성할 수 있는 매우 편리한 기능을 많이 제공한다.

태스트 코드 개선

@Before

테스트를 실행할 때마다 반복되는 준비 작업을 별도의 메소드에 넣게 해주고, 이를 매번 메소드를 실행하기 전에 먼저 실행시켜주는 기능

JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다. 1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다. 2. 테스트 클래스의 오브젝트를 하나 만든다. 3. @Before가 붙은 메소드가 있으면 실행한다. 4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다. 5. @After가 붙은 메소드가 있으면 실행한다. 6. 나머지 테스트 메소드에 대해 2~5번을 반복한다. 7. 모든 테스트의 결과를 종합해서 돌려준다.

주의할 점은 @Before@After메소드를 텧스트 메소드에서 직접 호출하지 않기 때문에 서로 주고받을 정보나 오브젝트가 있다면 인스턴스 변수를 이용해야 한다.

또한 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다는 것이다. 이는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다.

픽스쳐

테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스쳐(fixture)라고 한다. UserDaoTest에서라면 dao가 대표적인 픽스처라고 볼 수 있다.